/*
 * 版权所有 (C) 2015 知启蒙(ZHIQIM) 保留所有权利。[遇见知启蒙，邂逅框架梦，本文采用Apache-2.0许可证]
 * 
 * https://zhiqim.org/project/zhiqim_products/zhiqim_lucener.htm
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.zhiqim.lucener;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.DoubleField;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.FloatField;
import org.apache.lucene.document.IntField;
import org.apache.lucene.document.LongField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexNotFoundException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.classic.MultiFieldQueryParser;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.zhiqim.kernel.Global;
import org.zhiqim.kernel.Servicer;
import org.zhiqim.kernel.annotation.AnAlias;
import org.zhiqim.kernel.config.Group;
import org.zhiqim.kernel.config.Item;
import org.zhiqim.kernel.constants.SignConstants;
import org.zhiqim.kernel.extend.MapSS;
import org.zhiqim.kernel.json.Jsons;
import org.zhiqim.kernel.logging.Log;
import org.zhiqim.kernel.logging.LogFactory;
import org.zhiqim.kernel.paging.PageBuilder;
import org.zhiqim.kernel.paging.PageResult;
import org.zhiqim.kernel.util.Arrays;
import org.zhiqim.kernel.util.Asserts;
import org.zhiqim.kernel.util.Files;
import org.zhiqim.kernel.util.Ids;
import org.zhiqim.kernel.util.Ints;
import org.zhiqim.kernel.util.Lists;
import org.zhiqim.kernel.util.Longs;
import org.zhiqim.kernel.util.Strings;
import org.zhiqim.kernel.util.Validates;

/**
 * 基于Lucene的搜索服务
 *
 * @version v1.0.0 @author zouzhigang 2018-4-14 新建与整理
 */
@AnAlias("LuceneServer")
public class LuceneServer extends Servicer implements SignConstants
{
    private static final Log log = LogFactory.getLog(LuceneServer.class);
    
    private int idLen;
    private String rootDir;
    
    private Directory directory;
    private List<LuceneField> fieldList;
    private SortField[] sortFields;
    
    private Analyzer analyzer;
    
    /*******************************************************************************************/
    //创建&销毁服务
    /*******************************************************************************************/
    
    @Override
    public boolean create() throws Exception
    {
        Group group = Global.getGroup(id);
        if (group == null)
        {
            log.error("搜索服务[%s]未找到配置", id);
            return false;
        }
        
        //1.检查索引目录
        idLen = group.getInt("idLen", 16);
        if (idLen != 13 && idLen != 16 && idLen != 19 && idLen != 32)
        {
            log.error("搜索服务[%s][idLen]只支持13|16|19|32四种，可以不配置，默认16", id);
            return false;
        }
        
        String rootDir = group.getString("rootDir");
        if (Validates.isEmpty(rootDir))
        {
            log.error("搜索服务[%s][rootDir]不能为空", id);
            return false;
        }
        
        File dir = new File(rootDir);
        this.rootDir = Files.toLinuxPath(dir.getCanonicalPath());
        if (!Files.mkDirectory(dir))
        {
            log.error("搜索服务[%s][directory]不存在且创建失败", id);
            return false;
        }
        
        this.directory = FSDirectory.open(dir.toPath());
        
        //2.检查索引字段
        fieldList = new ArrayList<>();
        Group fGroup = Global.getGroup(id+".field");
        Collection<Item> feilds = fGroup.list();
        for (Item item : feilds)
        {
            if (_ID_.equals(item.getKey()))
            {
                log.error("搜索服务[%s.field][id]为系统默认字段，不允许自定义配置字段", id);
                return false;
            }
            
            LuceneField field = Jsons.toObject(item.getString(), LuceneField.class);
            field.setName(item.getKey());
            field.setDesc(item.getDesc());
            fieldList.add(field);
        }
        
        Lists.trim(fieldList);
        
        //3.检查排序字段
        List<LuceneSort> sortList = new ArrayList<>();
        Group sGroup = Global.getGroup(id+".sort");
        Collection<Item> sorts = sGroup.list();
        for (Item item : sorts)
        {
            LuceneSort sort = Jsons.toObject(item.getString(), LuceneSort.class);
            sort.setName(item.getKey());
            sort.setDesc(item.getDesc());
            sortList.add(sort);
        }
        
        sortFields = new SortField[sortList.size()];
        for (int i=0;i<sortList.size();i++)
        {
            LuceneSort sort = sortList.get(i);
            SortField.Type type = sort.getSortType();
            if (type == null)
            {
                log.error("搜索服务[%s.sort][%s]的[type]当前仅支持score|doc|int|long|float|double六种", id, sort.getName());
                return false;
            }
            
            if (type == SortField.Type.SCORE)
                sortFields[i] = SortField.FIELD_SCORE;//不支持配置倒序
            else if (type == SortField.Type.DOC)
                sortFields[i] = new SortField(null, SortField.Type.DOC, sort.isReverse());
            else
            {//除score/doc外，要判断字段类型
                LuceneField field = getField(sort.getName());
                if (field == null)
                {
                    log.error("搜索服务[%s.sort][%s]的不是字段", id, sort.getName());
                    return false;
                }
                
                if (!field.getType().equals(sort.getType()))
                {
                    log.error("搜索服务[%s.sort][%s]的字段类型不一致", id, sort.getName());
                    return false;
                }
                
                field.setSort(true);//设置该字段为需要排序，在增加文档是设置支持排序才行
                sortFields[i] = new SortField(sort.getName(), type, sort.isReverse());
            }
        }
        
        //4.中文分词器
        String className = group.getString("analyzer");
        if (Validates.isEmpty(className))
        {
            analyzer = new SmartChineseAnalyzer();
        }
        else
        {
            Class<?> clazz = Class.forName(className);
            analyzer = (Analyzer) clazz.newInstance();
        }
        
        return true;
    }
    
    @Override
    public void destroy()
    {
        if (directory != null)
        {
            try{directory.close();}catch (IOException e){}
            directory = null;
        }
    }
    
    /*******************************************************************************************/
    //获取参数
    /*******************************************************************************************/
    
    public int getIdLen()
    {
        return idLen;
    }
    
    public String getRootDir()
    {
        return rootDir;
    }

    public Directory getDirectory()
    {
        return directory;
    }
    
    public List<LuceneField> getFieldList()
    {
        return fieldList;
    }
    
    public LuceneField getField(String name)
    {
        for (LuceneField field : fieldList)
        {
            if (field.getName().equals(name))
                return field;
        }
        
        return null;
    }
    
    public SortField[] getSortFields()
    {
        return sortFields;
    }
    
    public boolean hasSortField()
    {
        return sortFields.length > 0;
    }
    
    /********************************************************************************************/
    //item&insert&update&delete 数据
    /********************************************************************************************/
    
    /**
     * 查询一条数据
     * 
     * @param id            主键
     * @throws IOException  IO异常
     */
    public Document item(String id) throws IOException
    {
        try(IndexReader reader = getReader())
        {
            if (reader == null)
                return null;
            
            IndexSearcher searcher = new IndexSearcher(reader);
            TopDocs docs = searcher.search(getIdQuery(id), 1);
            if (docs == null || docs.totalHits == 0)
                return null;
            
            return searcher.doc(docs.scoreDocs[0].doc);
        }
    }
    
    /**
     * 插入一条数据
     * 
     * @param map           数据表
     * @throws IOException  IO异常
     */
    public void insert(MapSS map) throws IOException
    {
        String id = map.get(_ID_);
        boolean isBuild = false;
        if (Validates.isEmptyBlank(id))
        {
            id = buildId();
            isBuild = true;
        }
        
        //检查ID是否已存在，保证唯一
        Document doc = item(id);
        Asserts.as(doc == null?null:"插入数据时["+id+"]已存在，" + (isBuild?"生成的id有重复，请联系管理检查":"传入的id有重复，如果已存在请修改"));
        
        //1.索引编号类型
        doc = new Document();
        doc.add(new StringField(_ID_, id, Store.YES));
        
        //2.其他索引字段
        for (LuceneField field : fieldList)
        {
            String value = map.get(field.getName());
            if (Validates.isEmptyBlank(value))
                Asserts.asserts(!field.isRequired(), "插入数据时必须的字段[%s]不能为空白", field.getName());
            else
                addField(doc, field, value);
        }

        //3.中文分词并保存
        try (IndexWriter writer = new IndexWriter(directory, getConfigChinese());)
        {
            writer.addDocument(doc);
            writer.forceMergeDeletes();
            writer.forceMerge(7);
            writer.commit();
        }
    }
    
    /**
     * 修改一条数据
     * 
     * @param map           数据表
     * @throws IOException  IO异常
     */
    public void update(MapSS map) throws IOException
    {
        String id = map.get(_ID_);
        Asserts.as(Validates.isNotEmptyBlank(id)?null:"修改数据时未传入[id]的值");
        
        //1.先查出文档
        Document doc = item(id);
        Asserts.as(doc != null?null:"修改数据时未找到原文档["+id+"]");
         
        //2.其他索引字段
        for (LuceneField field : fieldList)
        {
            String value = map.get(field.getName());
            if (value == null)
            {//字段未传入，表示不修改
                continue;
            }
            
            if (Validates.isEmptyBlank(value))
            {//字段为空白验证必须字段
                Asserts.asserts(!field.isRequired(), "修改数据时必须的字段[%s]不能为空白", field.getName());
            }
            
            doc.removeField(field.getName());
            addField(doc, field, value);
        }

        //3.中文分词并保存
        try (IndexWriter writer = new IndexWriter(directory, getConfigChinese());)
        {
            writer.updateDocument(new Term(_ID_, id), doc);
            writer.forceMergeDeletes();
            writer.forceMerge(7);
            writer.commit();
        }
    }

    /**
     * 删除一条数据
     * 
     * @param id            主键
     * @throws IOException  IO异常
     */
    public void delete(String id) throws IOException
    {
        try (IndexWriter writer = new IndexWriter(directory, getConfig());)
        {
            writer.deleteDocuments(getIdQuery(id));
            writer.forceMergeDeletes();
            writer.forceMerge(7);
            writer.commit();
        }
    }
    
    /********************************************************************************************/
    //page 分页显示
    /********************************************************************************************/
    
    /**
     * 查询表对象分页信息
     * 
     * @param pageNo        页码
     * @param pageSize      页数
     * @param q             查询条件
     * @return              分页信息,包括总页数,页码,页数和查询的记录
     * @throws IOException  IO异常
     * @throws ParseException 
     */
    public PageResult<Document> page(int pageNo, int pageSize, String q) throws IOException, ParseException
    {
        return page(pageNo, pageSize, q, sortFields);
    }
    
    /**
     * 查询表对象分页信息
     * 
     * @param pageNo        页码
     * @param pageSize      页数
     * @param q             查询条件
     * @param fieldName     排序字段
     * @param reverse       是否倒序
     * @return              分页信息,包括总页数,页码,页数和查询的记录
     * @throws IOException  IO异常
     * @throws ParseException
     */
    public PageResult<Document> page(int pageNo, int pageSize, String q, String fieldName, boolean reverse) throws IOException, ParseException
    {
        if (Validates.isEmpty(fieldName))
            return page(pageNo, pageSize, q, sortFields);
        
        SortField sortField = null;
        for (SortField item : sortFields)
        {
            if (fieldName.equals(item.getField()))
            {
                sortField = new SortField(fieldName, item.getType(), reverse);
                break;
            }
        }
        
        if (sortField == null)
            return page(pageNo, pageSize, q, sortFields);
        
        SortField[] sorts = new SortField[]{sortField};
        return page(pageNo, pageSize, q, sorts);
    }
    
    /**
     * 查询表对象分页信息
     * 
     * @param pageNo        页码
     * @param pageSize      页数
     * @param q             查询条件
     * @param sortFields    排序列表
     * @return              分页信息,包括总页数,页码,页数和查询的记录
     * @throws IOException  IO异常
     * @throws ParseException
     */
    public PageResult<Document> page(int pageNo, int pageSize, String q, SortField[] sortFields) throws IOException, ParseException
    {
        if (Validates.isEmptyBlank(q))
            q = "*:*";
        
        try (IndexReader reader = getReader())
        {
            if (reader == null)
                return PageBuilder.newResult(pageNo, pageSize);

            //1.组装查询器
            List<String> nameList = new ArrayList<>();
            if(Validates.isInteger(q))
                nameList.add(_ID_);
            
            Map<String,Float> boosts = new HashMap<>();
            for (LuceneField field : fieldList)
            {
                if (!field.isIndexable())
                    continue;
                
                nameList.add(field.getName());
                boosts.put(field.getName(), field.getWeight());
            }
            String[] fields = Arrays.toStringArray(Lists.toString(nameList));
            QueryParser qp = new MultiFieldQueryParser(fields, analyzer, boosts);
            Query query = qp.parse(q);
            
            //2.先查总数，用于分页
            IndexSearcher searcher = new IndexSearcher(reader);
            int total = searcher.count(query);
            if (total < (pageNo-1)*pageSize+1)
            {//小于要求的页数
                return PageBuilder.newResult(total, pageNo, pageSize, new ArrayList<Document>());
            }
            
            //3.再查数据列表
            TopDocs docs = null;
            if (hasSortField())
                docs = searcher.search(query, pageNo*pageSize, new Sort(sortFields));
            else
                docs = searcher.search(query, pageNo*pageSize);
            
            List<Document> list = new ArrayList<>();
            for (int i=(pageNo-1)*pageSize;i<pageNo*pageSize && i<total;i++)
            {
                Document document = searcher.doc(docs.scoreDocs[i].doc);
                list.add(document);
            }
            
            //4.组装分页对象
            return PageBuilder.newResult(total, pageNo, pageSize, list);
        }
    }

    /********************************************************************************************/
    //常用方法
    /********************************************************************************************/
    
    /** 获取ID的查询条件 */
    public String buildId()
    {
        switch (idLen)
        {
        case 13: return Strings.valueOf(Ids.longId13());
        case 16: return Strings.valueOf(Ids.longId());
        case 19: return Strings.valueOf(Ids.longId19());
        default: return Ids.uuid().toLowerCase();
        }
    }
    
    /** 增加字段 */
    public void addField(Document doc, LuceneField field, String value)
    {
        if ("long".equals(field.getType()))
            doc.add(new LongField(field.getName(), Longs.toLong(value), field.isSort()?LuceneFieldType.TYPE_STORED_SORTED_LONG:LongField.TYPE_STORED));
        else if ("int".equals(field.getType()))
            doc.add(new IntField(field.getName(), Ints.toInt(value), field.isSort()?LuceneFieldType.TYPE_STORED_SORTED_INT:IntField.TYPE_STORED));
        else if ("float".equals(field.getType()))
            doc.add(new FloatField(field.getName(), Float.parseFloat(value), field.isSort()?LuceneFieldType.TYPE_STORED_SORTED_FLOAT:FloatField.TYPE_STORED));
        else if ("double".equals(field.getType()))
            doc.add(new DoubleField(field.getName(), Double.parseDouble(value), field.isSort()?LuceneFieldType.TYPE_STORED_SORTED_DOUBLE:DoubleField.TYPE_STORED));
        else if ("string".equals(field.getType()))
            doc.add(new StringField(field.getName(), value, Store.YES));
        else
            doc.add(new TextField(field.getName(), value, Store.YES));
    }
    
    /** 获取ID的查询条件 */
    public Query getIdQuery(String id)
    {
        return new TermQuery(new Term(_ID_, id));
    }
    
    /** 获取索引读 */
    public IndexReader getReader() throws IOException
    {
        try
        {
            return DirectoryReader.open(getDirectory());
        }
        catch (IndexNotFoundException e)
        {
            return null;
        }
    }
    
    /** 获取中文分词配置 */
    public IndexWriterConfig getConfigChinese()
    {
        return new IndexWriterConfig(analyzer);
    }
    
    /** 获取标准配置 */
    public IndexWriterConfig getConfig()
    {
        return new IndexWriterConfig(new StandardAnalyzer());
    }
}
